﻿using Ionic.Zip;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using Windows.ApplicationModel;
using Windows.Management.Deployment;

namespace CorpStoreServer
{
    static class GenLib
    {
        /// <summary>
        /// The following external declarations are required to present a console window within a windows app.
        /// CorpStoreServer has been designed to be a 'windows' app so that it does not show a console window
        /// every time it is run via an extension type association.  There are times, however, when showing a
        /// console window is desired (e.g. when the master catalog is being updated).
        /// </summary>
        [DllImport("shlwapi.dll", BestFitMapping = false, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = false, ThrowOnUnmappableChar = true)]
        private static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, IntPtr ppvReserved);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool AllocConsole();

        [DllImport("kernel32.dll")]
        static extern IntPtr GetConsoleWindow();

        [DllImport("user32.dll")]
        static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        const int SW_HIDE = 0;
        const int SW_SHOW = 5;

        /// <summary>
        /// This function returns a single parameter with name 'keyName' from a
        /// name/value collection.
        /// </summary>
        /// <param name="coll">name/value collection of parameters</param>
        /// <param name="keyName">name of parameter to obtain</param>
        /// <returns>value of parameter</returns>
        public static string GetParm(NameValueCollection coll, string keyName)
        {
            return coll.Get(keyName) ?? "";
        }

        /// <summary>
        /// Returns date of a directory with name 'directoryName'
        /// </summary>
        /// <param name="directoryName"></param>
        /// <returns></returns>
        public static DateTime GetDirectoryDate(string directoryName)
        {
            DateTime date = new DateTime(1980, 1, 1);

            try
            {
                date = System.IO.Directory.GetLastWriteTime(directoryName);
            }
            catch (Exception)
            {}

            return date;
        }


        /// <summary>
        /// returns a list of all installed packages for currently logged in user
        /// </summary>
        /// <param name="installedPackages">list of installed packages for logged in user</param>
        /// <param name="errStr">description of any error encountered</param>
        /// <returns>true if list of packages was loaded ok</returns>
        public static bool LoadAllPackagesForUser(out List<Package> installedPackages, out string errStr)
        {
            bool blnOK = false;
            errStr = "";

            try
            {
                PackageManager manager = new PackageManager();
                installedPackages = manager.FindPackagesForUser("").ToList<Package>();

                blnOK = true;
            }
            catch (Exception ex)
            {
                errStr = string.Format("Unhandled exception in loadAllPackagesForUser: {0}", ex.Message);
                installedPackages = null;
            }

            return blnOK;
        }

        /// <summary>
        /// deletes all files which match a search pattern 'searchPattern' which 
        /// exist in a single directory 'directoryName'.  'searchPattern'
        /// is the wildcard pattern (e.g. '*.*').
        /// </summary>
        /// <param name="directoryName">directory from which files will be deleted</param>
        /// <param name="searchPattern">wildcard match pattern defining which files are to be deleted (e.g. *.*)</param>
        /// <param name="errStr">description of any error encountered</param>
        /// <returns>true if there were no exceptions while deleting files</returns>
        public static bool deleteFilesInDirectory(string directoryName, string searchPattern, out string errStr)
        {
            bool blnOK = true;

            errStr = "";
            try
            {
                if (Directory.Exists(directoryName))
                {
                    // directory exists - delete all files in it
                    IEnumerable<string> fileNames = Directory.EnumerateFiles(directoryName, searchPattern);

                    foreach (string fn in fileNames)
                    {
                        File.Delete(fn);
                    }
                }
            }
            catch (Exception ex)
            {
                errStr = string.Format("Unhandled exception in deleteFilesInDirectory '{0}' : {1}", directoryName, ex.Message);
                blnOK = false;
            }

            return blnOK;
        }

        /// <summary>
        /// Attempts to find an install app whose display name is 'displayName'.  Note that
        /// 'displayName' is *not* a unique identifier for an app.  Therefore, be careful 
        /// when using this function since it will only return only the first app which has
        /// the display name to find.
        /// </summary>
        /// <param name="displayName">human readable display name of app to find</param>
        /// <returns>Package structure containing structure of data defining the installed app</returns>
        public static Package FindPackageByDisplayName(string displayName)
        {
            Package foundPackage = null;
            PackageManager mgr = new PackageManager();
            List<Package> installedPackages = mgr.FindPackagesForUser(WindowsIdentity.GetCurrent().User.Value).ToList<Package>();
            string familyName = getFamilyNameForDisplayName(displayName);

            foreach (var package in installedPackages)
            {
                if (package.Id.FamilyName.Equals(familyName, StringComparison.OrdinalIgnoreCase))
                {
                    foundPackage = package;
                    break;
                }
            }

            return foundPackage;
        }

        /// <summary>
        /// Attempts to find an install app whose Package Family Name (PFN) is 'familyName'.
        /// </summary>
        /// <param name="familyName">Package Family Name to find (PFN)</param>
        /// <returns>Package structure containing structure of data defining the installed app</returns>
        public static Package FindPackageByFamilyName(string familyName)
        {
            Package foundPackage = null;
            PackageManager mgr = new PackageManager();
            List<Package> installedPackages = mgr.FindPackagesForUser(WindowsIdentity.GetCurrent().User.Value).ToList<Package>();

            foreach (var package in installedPackages)
            {
                if (package.Id.FamilyName.Equals(familyName, StringComparison.OrdinalIgnoreCase))
                {
                    foundPackage = package;
                    break;
                }
            }

            return foundPackage;
        }

        /// <summary>
        /// Use this function when you have a Package and you need to know its identity
        /// and version string.  The identity is typically used only when activating 
        /// (i.e. launching) an app.
        /// </summary>
        /// <param name="pkg">package to get details about</param>
        /// <param name="identity">unique package identity</param>
        /// <param name="version">version number string which gets defined</param>
        /// <param name="errStr">error string if problems in this function</param>
        /// <returns>true if details obtained ok</returns>
        public static bool GetPackageDetails(Package pkg, out string identity, out string version, out string errStr)
        {
            bool blnOK = false;
            identity = ""; version = ""; errStr = "";

            if (pkg != null)
            {
                // found a candidate package - return it
                PackageInfo info = new PackageInfo();
                if (info.Init(pkg, out errStr))
                {
                    identity = info.Identity;
                    version = info.Version;
                    blnOK = true;
                }
                else
                    errStr = string.Format("Error obtaining package info for package {0}", pkg.Id.FamilyName);
            }

            return blnOK;
        }

        /// <summary>
        /// This function attempts to obtain a package family name given a display
        /// name.  Note that this function returns the family name of the FIRST
        /// package it finds with this display name.  Also note that this function
        /// requires registry read access since the registry is used to obtain 
        /// this info.
        /// </summary>
        /// <param name="displayName">package display name (not case sensitive)</param>
        /// <returns>family name or empty string if not found</returns>
        public static string getFamilyNameForDisplayName(string displayName)
        {
            string familyName = "";

            string registryKey = @"Local Settings\Software\Microsoft\Windows\CurrentVersion\AppContainer\Mappings";
            using (RegistryKey key = Registry.ClassesRoot.OpenSubKey(registryKey))
            {
                foreach (String subkeyName in key.GetSubKeyNames())
                {
                    string dispName = key.OpenSubKey(subkeyName).GetValue("DisplayName").ToString();
                    if (dispName.Equals(displayName, StringComparison.OrdinalIgnoreCase))
                    {
                        familyName = key.OpenSubKey(subkeyName).GetValue("Moniker").ToString();
                        break;
                    }
                }
            }

            return familyName;
        }

        /// <summary>
        /// This function extracts a single file named 'filenameToExtract' contained 
        /// in a zip file to a destination directory 'outputDirectory'.  The 
        /// destination filename will match the source filename unless 'overrideDestFn'
        /// is non blank.  
        /// </summary>
        /// <param name="zipFileName">path + filename of zip file</param>
        /// <param name="filenameToExtract">source filename </param>
        /// <param name="outputDirectory"></param>
        /// <param name="overrideDestFn"></param>
        /// <param name="errStr">error string if problems</param>
        /// <returns>true if extracted ok</returns>
        public static bool ExtractZipFileToDirectory(string zipFileName, string filenameToExtract, string outputDirectory, string overrideDestFn, out string errStr)
        {
            return ExtractZipFileToDirectory(zipFileName, new List<string>() { filenameToExtract }, outputDirectory, overrideDestFn, out errStr);
        }


        /// <summary>
        /// Extracts a set of files 'filenamesToExtract' from zip file 'zipFileName' 
        /// to the directory 'outputDirectory'.  The target filename will be set to 
        /// 'overrideDestFn' if it is non blank; else the target filenames will be
        /// set to the same name as the source.
        /// </summary>
        /// <param name="zipFileName">zip filename to extract from</param>
        /// <param name="filenamesToExtract">list of files to extract</param>
        /// <param name="outputDirectory">destination directory</param>
        /// <param name="overrideDestFn">target filename</param>
        /// <param name="errStr">error string if problems</param>
        /// <returns>true if at least one file is extracted ok and no exceptions</returns>
        public static bool ExtractZipFileToDirectory(string zipFileName, List<string> filenamesToExtract, string outputDirectory, string overrideDestFn, out string errStr)
        {
            bool blnOK = false;

            errStr = "";
            try
            {
                ZipFile zip = ZipFile.Read(zipFileName);
                Directory.CreateDirectory(outputDirectory);
                foreach (ZipEntry e in zip)
                {
                    // check if you want to extract e or not
                    if (filenamesToExtract.Where(fn => fn.Equals(e.FileName, StringComparison.OrdinalIgnoreCase)).SingleOrDefault() != null)
                    {
                        if (overrideDestFn != "")
                            e.FileName = overrideDestFn;

                        e.Extract(outputDirectory, ExtractExistingFileAction.OverwriteSilently);
                        blnOK = true;
                    }
                }
                zip.Dispose();
            }
            catch (Exception ex)
            {
                errStr = string.Format("Unhandled exception in ExtractZipFileToDirectory: {0}", ex.Message);
                blnOK = false;
            }

            return blnOK;
        }


        /// <summary>
        /// Returns either an xml element's content (inner text) or its attribute,
        /// depending on whether 'attributeName' is non blank.  
        /// </summary>
        /// <param name="xml">xml document which has been preloaded</param>
        /// <param name="elementName">name of xml element</param>
        /// <param name="attributeName">element attribute name to return; set to empty string if desiring element contents</param>
        /// <param name="errStr">error string if problems or if element not found</param>
        /// <returns>element value or attribute or blank if element not found</returns>
        public static string parseElement(XmlDocument xml, string elementName, string attributeName, out string errStr)
        {
            XmlNodeList nodes;
            string value = "";
            errStr = "";

            try
            {
                nodes = xml.GetElementsByTagName(elementName, "*");
                if (nodes.Count > 0)
                {
                    if (attributeName != "")
                        value = ((XmlElement)nodes[0]).GetAttribute(attributeName);
                    else
                        value = ((XmlElement)nodes[0]).InnerText;
                }
                else
                {
                    errStr = string.Format("Cannot find element '{0} within mainfest file", elementName);
                }
            }
            catch (Exception ex)
            {
                errStr = string.Format("Unhandled exception in parseElement: {0}", ex.Message);
            }

            return value;
        }


        /// <summary>
        /// This function attempts to obtain the value of a xml element or attribute of 
        /// that element, assuming that the xml document 'doc' has been populated with
        /// the contents of a windows 8 app package manifest.  This is similar to the 
        /// parseElement function except that this function will attempt to obtain the
        /// value from a resource associated with the app package 'pkg' IF the value
        /// of the element starts with 'ms-resource'.
        /// </summary>
        /// <param name="pkg">app package associated with the xml document 'doc' which has been loaded from its app manifest</param>
        /// <param name="doc">xml document loaded from the 'pkg's appxmanifest</param>
        /// <param name="elementName">name of element within 'doc' to obtain value for</param>
        /// <param name="attributeName">name of attribute of element to obtain value for; leave blank if desiring element content</param>
        /// <param name="errStr">error string if problems</param>
        /// <returns>value of element</returns>
        public static string GetManifestElementValue(Package pkg, XmlDocument doc, string elementName, string attributeName, out string errStr)
        {
            return GetResourcedElementValue(pkg.Id.FullName, pkg.Id.Name, doc, elementName, attributeName, out errStr);
        }


        public static string GetResourcedElementValue(string priFilename, string pkgName, XmlDocument doc, string elementName, string attributeName, out string errStr)
        {
            string value = "";

            value = GenLib.parseElement(doc, elementName, attributeName, out errStr);
            if (value != null && value.StartsWith("ms-resource:", StringComparison.OrdinalIgnoreCase))
            {
                // this is a resource - resolve it
                string resString;
                if (value.Contains('/'))
                    resString = string.Format(@"@{{{0}? {1}}}", priFilename, value);
                else
                    resString = string.Format(@"@{{{0}? ms-resource://{1}/resources/{2}}}", priFilename, pkgName, value.Replace("ms-resource:", ""));
                var outBuff = new StringBuilder(1024);
                int result = SHLoadIndirectString(resString, outBuff, outBuff.Capacity, IntPtr.Zero);
                if (result != 0)
                    errStr = string.Format("Error {0} while trying to load resource '{1}'", result, value);
                value = outBuff.ToString();
            }

            return value;
        }


        /// <summary>
        /// Attempts to find a file within a zip file given a regular expression 'filenameSearchPattern'.
        /// </summary>
        /// <param name="zipFileName">zip file to search</param>
        /// <param name="filenameSearchPattern">regular expression to match when searching for a file</param>
        /// <param name="foundFn">full name of file found within zip</param>
        /// <param name="errStr">error string if problems</param>
        /// <returns>true if at least one file within the zip match the filenameSearchPattern</returns>
        internal static bool FindFilenameInZip(string zipFileName, string filenameSearchPattern, out string foundFn, out string errStr)
        {
            bool blnOK = false;
            errStr = ""; foundFn = "";

            try
            {
                ZipFile zip = ZipFile.Read(zipFileName);
                foreach (ZipEntry e in zip)
                {
                    // check if you want to extract e or not
                    if (Regex.IsMatch(e.FileName, filenameSearchPattern))
                    {
                        foundFn = e.FileName;
                        blnOK = true;
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                errStr = string.Format("Unhandled exception in FindFilenameInZip: {0}", ex.Message);
            }

            return blnOK;
        }


        /// <summary>
        /// Replaces the following substrings contained in 'inputStr' with actual values derived from the system:
        ///   %USERPROFILE%      => user profile directory
        ///   %HOMEDIRECTORY%    => user profile directory
        ///   %MYDOCUMENTS%      => user's my documents directory
        ///   %APPDATA%          => user's application data directory
        ///   %SERVERDIRECTORY%  => directory of currently running exe
        /// </summary>
        /// <param name="inputStr">string containing mix of literals and substrings above</param>
        /// <param name="errStr">error string if problems</param>
        /// <returns>expanded form of inputStr with variable substrings replaced with actual contents</returns>
        public static string ExpandEnvironmentVars(string inputStr, out string errStr)
        {
            string newStr = inputStr.ToUpper();

            errStr = "";
            try
            {
                newStr = newStr.Replace("%USERPROFILE%", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
                newStr = newStr.Replace("%HOMEDIRECTORY%", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
                newStr = newStr.Replace("%MYDOCUMENTS%", Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
                newStr = newStr.Replace("%APPDATA%", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
                newStr = newStr.Replace("%SERVERDIRECTORY%", AppExePath());
            }
            catch (Exception ex)
            {
                errStr = string.Format("Unhandled exception in expandEnvironmentVars: {0}", ex.Message);
            }

            return newStr;
        }


        /// <summary>
        /// Attempts to allocate a new console window if none exists, or activate
        /// existing one.  This is typically used to show a console window for
        /// applications which are not officially classified as console applications.
        /// </summary>
        public static void ShowConsoleWindow()
        {
            var handle = GetConsoleWindow();

            if (handle == IntPtr.Zero)
            {
                AllocConsole();
            }
            else
            {
                ShowWindow(handle, SW_SHOW);
            }
        }

        /// <summary>
        /// Hides an existing console window shown by ShowConsoleWindow
        /// </summary>
        public static void HideConsoleWindow()
        {
            var handle = GetConsoleWindow();

            ShowWindow(handle, SW_HIDE);
        }


        /// <summary>
        /// Returns the total number of lines contained in text file 'fn'.  
        /// </summary>
        /// <param name="fn">path + filename to search</param>
        /// <param name="errStr">error string if problems</param>
        /// <returns>total number of lines contained in file or 0 if file does not exist</returns>
        public static long CountLines(string fn, out string errStr)
        {
            long count = -1;

            errStr = "";
            try
            {
                if (File.Exists(fn))
                    count = File.ReadLines(fn).Count();
                else
                    count = 0;
            }
            catch (Exception ex)
            {
                errStr = string.Format("Unhandled exception in CountLines: {0}", ex.Message);
            }

            return count;
        }

        /// <summary>
        /// returns the full path to the currently executing assembly
        /// </summary>
        /// <returns></returns>
        public static string AppExePath()
        {
            return Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
        }

        /// <summary>
        /// returns the exe path+filename which is currently running
        /// </summary>
        /// <returns></returns>
        public static string AppExeFn()
        {
            return System.Reflection.Assembly.GetExecutingAssembly().Location;
        }

        /// <summary>
        /// returns the currently running assembly's version number in string form.  This
        /// is useful when displaying version information in an about window or window 
        /// caption.
        /// </summary>
        /// <returns></returns>
        public static string AppExeVer()
        {
            return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
        }


        /// <summary>
        /// returns a SHA1 hash of the contents of file 'fileName'.  This is useful
        /// when you need to be able to identify when the contents of a file has
        /// changed.  This hash value will change even if just a single character
        /// is modified in 'fileName'.
        /// </summary>
        /// <param name="fileName">filename which contains contents for hash function</param>
        /// <param name="errStr">error string if problems</param>
        /// <returns>SHA1 hash value</returns>
        public static string GetFileHash(string fileName, out string errStr)
        {
            string hash = "";

            errStr = "";
            try
            {
                HashAlgorithm sha1 = HashAlgorithm.Create();
                using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
                    hash = Convert.ToBase64String(sha1.ComputeHash(stream));
            }
            catch (Exception ex)
            {
                errStr = string.Format("Unhandled exception in GetFileHash: {0}", ex.Message);
            }
            
            return hash;
        }

        /// <summary>
        /// return a string representation of a win8 app package version structure.
        /// </summary>
        /// <param name="version">version structure from app package (Package)</param>
        /// <returns>version string</returns>
        public static string GetVersionString(PackageVersion version)
        {
            return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
        }
    } // GenLib
}
